/*
 * JBoss, Home of Professional Open Source Copyright 2011, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a full listing of individual
 * contributors. This is free software; you can redistribute it and/or modify it under the terms of
 * the GNU Lesser General Public License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version. This software is distributed
 * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * for more details. You should have received a copy of the GNU Lesser General Public License along
 * with this software; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth
 * Floor, Boston, MA 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package gov.fgn.sijyp.general.util;

import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;

import javax.faces.context.FacesContext;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.ajax4jsf.model.DataVisitor;
import org.ajax4jsf.model.ExtendedDataModel;
import org.ajax4jsf.model.Range;
import org.ajax4jsf.model.SequenceRange;
import org.apache.log4j.Logger;
import org.richfaces.component.SortOrder;
import org.richfaces.model.Arrangeable;
import org.richfaces.model.ArrangeableState;
import org.richfaces.model.FilterField;
import org.richfaces.model.SortField;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;

public abstract class JPADataModel<T> extends ExtendedDataModel<T> implements
		Arrangeable {

	private Logger logSys = Logger.getLogger("SIJYP");

	private EntityManager entityManager;

	private Object rowKey;

	private ArrangeableState arrangeableState;

	private Class<T> entityClass;

	private boolean selectWithoutFilters;

	public JPADataModel(EntityManager entityManager, Class<T> entityClass) {
		super();
		this.entityClass = entityClass;
		this.entityManager = entityManager;
	}

		@Override
	public void arrange(FacesContext context, ArrangeableState state) {
		arrangeableState = state;
	}

	@Override
	public void setRowKey(Object key) {
		rowKey = key;
	}

	@Override
	public Object getRowKey() {
		return rowKey;
	}

	private CriteriaQuery<Long> createCountCriteriaQuery()
			throws NoSuchFieldException, ParseException {
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		CriteriaQuery<Long> criteriaQuery = criteriaBuilder
				.createQuery(Long.class);
		Root<T> root = criteriaQuery.from(entityClass);

		Expression<Boolean> filterCriteria = createFilterCriteria(
				criteriaBuilder, criteriaQuery, root);
		if (filterCriteria != null) {
			criteriaQuery.where(filterCriteria);

		}

		Expression<Long> count = criteriaBuilder.countDistinct(root);
		criteriaQuery.select(count);

		return criteriaQuery;
	}

	private CriteriaQuery<T> createSelectCriteriaQuery()
			throws NoSuchFieldException, ParseException {
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(entityClass);
		Root<T> root = criteriaQuery.from(entityClass);

		if (arrangeableState != null) {

			List<Order> orders = createOrders(criteriaBuilder, root);
			if (!orders.isEmpty()) {
				criteriaQuery.orderBy(orders);
			}

			Expression<Boolean> filterCriteria = createFilterCriteria(
					criteriaBuilder, criteriaQuery, root);
			if (filterCriteria != null) {
				criteriaQuery.where(filterCriteria);
			}
			//criteriaQuery.distinct(true);
		}

		return criteriaQuery;
	}

	private List<Order> createOrders(CriteriaBuilder criteriaBuilder,
			Root<T> root) {
		List<Order> orders = Lists.newArrayList();
		List<SortField> sortFields = arrangeableState.getSortFields();
		if (sortFields != null && !sortFields.isEmpty()) {

			FacesContext facesContext = FacesContext.getCurrentInstance();

			for (SortField sortField : sortFields) {
				String propertyName = (String) sortField.getSortBy().getValue(
						facesContext.getELContext());

				
				String[] properties = propertyName.split("\\.");
				
				Path<Object> expression = null;
				if(properties.length > 0) {
					expression = root.get(properties[0]);
					for(int i=1; i<properties.length; i++) {
						expression = expression.get(properties[i]);
					}
				} 
						
				
				
//				Path<Object> expression = root.get(propertyName);

				Order jpaOrder;
				SortOrder sortOrder = sortField.getSortOrder();
				if (sortOrder == SortOrder.ascending) {
					jpaOrder = criteriaBuilder.asc(expression);
				} else if (sortOrder == SortOrder.descending) {
					jpaOrder = criteriaBuilder.desc(expression);
				} else {
					throw new IllegalArgumentException(sortOrder.toString());
				}

				orders.add(jpaOrder);
			}
		}

		return orders;
	}

	protected ArrangeableState getArrangeableState() {
		return arrangeableState;
	}

	protected Class<T> getEntityClass() {
		return entityClass;
	}

	protected Expression<Boolean> createFilterCriteriaForStringField(
			String propertyName, Object filterValue, Path<T> root,
			CriteriaBuilder criteriaBuilder, boolean negate) {
		String stringFilterValue = (String) filterValue;
		if (Strings.isNullOrEmpty(stringFilterValue)) {
			return null;
		}

		stringFilterValue = stringFilterValue.toLowerCase(arrangeableState
				.getLocale());

		Path<String> expression = root.get(propertyName);

		if (this.entityClass.getName().equals("gov.fgn.sijyp.integracion.personas.Persona")
				|| this.entityClass.getName().equals("Postulado")) {

			Predicate disyunction = criteriaBuilder.disjunction();
			StringTokenizer st = new StringTokenizer(stringFilterValue);
			while (st.hasMoreTokens()) {
				// criteriaBuilder.
				Expression<Boolean> locator = criteriaBuilder.like(criteriaBuilder.lower(expression), "%"+st.nextToken().toLowerCase()+"%");
				disyunction.getExpressions().add(locator);
			}

		return disyunction;
		}
		else{
		Expression<Integer> locator = criteriaBuilder.locate(
				criteriaBuilder.lower(expression), stringFilterValue, 1);
		return negate ? criteriaBuilder.lessThanOrEqualTo(locator, 0)
				: criteriaBuilder.gt(locator, 0);
		}
	}

	protected Expression<Boolean> createFilterCriteriaForNumberField(
			String propertyName, Object filterValue, Path<T> root,
			CriteriaBuilder criteriaBuilder, boolean negate) {
		if (filterValue == null)
			return null;
		Number nuFilterValue = (Number.class.isAssignableFrom(filterValue
				.getClass())) ? (Number) filterValue : Double
				.parseDouble((String) filterValue);
		if (nuFilterValue == null || nuFilterValue.longValue() == 0)
			return null;
		Path<Number> expression = root.get(propertyName);
		return negate ? criteriaBuilder.notEqual(expression, nuFilterValue)
				: criteriaBuilder.equal(expression, nuFilterValue);
	}

	protected Expression<Boolean> createFilterCriteriaForDateField(
			String propertyName, Object filterValue, Path<T> root,
			CriteriaBuilder criteriaBuilder, boolean negate)
			throws ParseException {
		if (filterValue == null)
			return null;
		DateFormat df = DateFormat.getInstance();
		Date dFilter = (Date.class.isAssignableFrom(filterValue.getClass())) ? (Date) filterValue
				: df.parse((String) filterValue);
		if (dFilter == null)
			return null;

		Path<Date> expression = root.get(propertyName);
		return negate ? criteriaBuilder.notEqual(expression, dFilter)
				: criteriaBuilder.equal(expression, dFilter);
	}

	protected Expression<Boolean> createFilterCriteriaForJoinField(
			String propertyName, Object filterValue, Root<T> root,
			CriteriaBuilder criteriaBuilder, boolean negate)
			throws NoSuchFieldException, ParseException {
		if (negate) {
			propertyName = propertyName.substring(1);
		}
		StringTokenizer sTokenizer = new StringTokenizer(propertyName, ".");
		String propertyFieldToken = sTokenizer.nextToken();
		Join<T, T> joinP = root.join(propertyFieldToken);
		propertyFieldToken = sTokenizer.nextToken();
		while (sTokenizer.hasMoreTokens()) {
			joinP = joinP.join(propertyFieldToken);

			propertyFieldToken = sTokenizer.nextToken();
		}
		Class<?> fieldClass = ((Path<T>) joinP).getJavaType()
				.getDeclaredField(propertyFieldToken).getType();
		if (Number.class.isAssignableFrom(fieldClass)) {
			return createFilterCriteriaForNumberField(propertyFieldToken,
					filterValue, joinP, criteriaBuilder, negate);

		} else if (String.class.isAssignableFrom(fieldClass)) {
			return createFilterCriteriaForStringField(propertyFieldToken,
					filterValue, joinP, criteriaBuilder, negate);
		} else if (Date.class.isAssignableFrom(fieldClass)) {
			return createFilterCriteriaForDateField(propertyFieldToken,
					filterValue, joinP, criteriaBuilder, negate);
		}
		return null;

	}

	private Expression<Boolean> createFilterCriteria(
			CriteriaBuilder criteriaBuilder,
			@SuppressWarnings("rawtypes") CriteriaQuery criteriaQuery,
			Root<T> root) throws NoSuchFieldException, ParseException {
		Expression<Boolean> filterCriteria = null;
		List<FilterField> filterFields = arrangeableState.getFilterFields();
		if (filterFields != null && !filterFields.isEmpty()) {
			FacesContext facesContext = FacesContext.getCurrentInstance();

			for (FilterField filterField : filterFields) {
				String propertyName = (String) filterField
						.getFilterExpression().getValue(
								facesContext.getELContext());
				Object filterValue = filterField.getFilterValue();
				if (filterValue == null || filterValue.equals(""))
					continue;
				Expression<Boolean> predicate = null;
				boolean negate = propertyName.startsWith("!");

				if (!propertyName.contains(".")) {
					// Resolve the last criteria of a relations chain (must be a
					// primitive type)
					if (negate) {
						propertyName = propertyName.substring(1);
					}
					Class<?> fieldClass = ((Path<T>) root).getJavaType()
							.getDeclaredField(propertyName).getType();
					if (Number.class.isAssignableFrom(fieldClass)) {
						predicate = createFilterCriteriaForNumberField(
								propertyName, filterValue, root,
								criteriaBuilder, negate);

					} else if (String.class.isAssignableFrom(fieldClass)) {
						predicate = createFilterCriteriaForStringField(
								propertyName, filterValue, root,
								criteriaBuilder, negate);
					} else if (Date.class.isAssignableFrom(fieldClass)) {
						predicate = createFilterCriteriaForDateField(
								propertyName, filterValue, root,
								criteriaBuilder, negate);
					}
				} else {

					predicate = createFilterCriteriaForJoinField(propertyName,
							filterValue, root, criteriaBuilder, negate);
				}

				if (predicate == null) {
					continue;
				}

				if (filterCriteria == null) {
					filterCriteria = predicate.as(Boolean.class);
				} else {
					filterCriteria = criteriaBuilder.and(filterCriteria,
							predicate.as(Boolean.class));
				}
			}
		}
		return filterCriteria;
	}

	@Override
	public void walk(FacesContext context, DataVisitor visitor, Range range,
			Object argument) {
		CriteriaQuery<T> criteriaQuery = null;
		if ((!arrangeableState.getFilterFields().isEmpty() && !this
				.isFilterFieldValuesEmpty(arrangeableState.getFilterFields()))
				|| this.isSelectWithoutFilters()) {

			try {
				criteriaQuery = createSelectCriteriaQuery();
			} catch (NoSuchFieldException nse) {
				logSys.error(nse.getStackTrace());
			} catch (ParseException pe) {
				logSys.error(pe.getStackTrace());
			}
			TypedQuery<T> query = entityManager.createQuery(criteriaQuery);

			SequenceRange sequenceRange = (SequenceRange) range;
			if (sequenceRange.getFirstRow() >= 0 && sequenceRange.getRows() > 0) {
				query.setFirstResult(sequenceRange.getFirstRow());
				query.setMaxResults(sequenceRange.getRows());
			}

			List<T> data = query.getResultList();
			for (T t : data) {
				visitor.process(context, getId(t), argument);
			}
		}
	}

	@Override
	public boolean isRowAvailable() {
		return rowKey != null;
	}

	@Override
	public int getRowCount() {
		if ((arrangeableState != null
				&& arrangeableState.getFilterFields() != null
				&& !arrangeableState.getFilterFields().isEmpty() && !this
					.isFilterFieldValuesEmpty(arrangeableState
							.getFilterFields()))
				|| this.isSelectWithoutFilters()) {
			CriteriaQuery<Long> criteriaQuery = null;
			try {
				criteriaQuery = createCountCriteriaQuery();
				Integer totRecs = entityManager.createQuery(criteriaQuery)
						.getSingleResult().intValue();
				return totRecs;
			} catch (NoSuchFieldException nse) {
				logSys.error(nse.getStackTrace());
				return 0;
			} catch (ParseException pe) {
				logSys.error(pe.getStackTrace());
			}
		}
		return 0;
	}

	@Override
	public T getRowData() {
		return entityManager.find(entityClass, rowKey);
	}

	@Override
	public int getRowIndex() {
		return -1;
	}

	@Override
	public void setRowIndex(int rowIndex) {
		throw new UnsupportedOperationException();
	}

	@Override
	public Object getWrappedData() {
		throw new UnsupportedOperationException();
	}

	@Override
	public void setWrappedData(Object data) {
		throw new UnsupportedOperationException();
	}

	// TODO - implement using metadata
	protected abstract Object getId(T t);

	/**
	 * Determina si la lista de filtros ingresada tiene o no valores de filtro
	 * 
	 * @param filterFieldsParam
	 * @return Verdadero si la lista no tiene valores en los filtros.
	 * @author fabio.navarrete
	 */
	private boolean isFilterFieldValuesEmpty(List<FilterField> filterFieldsParam) {
		for (FilterField ff : filterFieldsParam) {
			if (ff.getFilterValue() != null) {
				return false;
			}
		}
		return true;
	}

	public boolean isSelectWithoutFilters() {
		return selectWithoutFilters;
	}

	public void setSelectWithoutFilters(boolean selectWithoutFilters) {
		this.selectWithoutFilters = selectWithoutFilters;
	}

}
